Load all necessary libraries¶

In [117]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import squarify
import folium
import json
import seaborn as sbn
from urllib.request import urlopen
import plotly.express as px
import warnings
warnings.filterwarnings('ignore')

Import data¶

In [118]:
fullData = pd.read_csv('valeursfoncieres-2020.txt', sep='|')

fullData2017 = pd.read_csv('valeursfoncieres-2017.txt', sep='|')
fullData2018 = pd.read_csv('valeursfoncieres-2018.txt', sep='|')
fullData2019 = pd.read_csv('valeursfoncieres-2019.txt', sep='|')
fullData2020 = pd.read_csv('valeursfoncieres-2020.txt', sep='|')
fullData2021 = pd.read_csv('valeursfoncieres-2021.txt', sep='|')
In [119]:
fullData
Out[119]:
Code service CH Reference document 1 Articles CGI 2 Articles CGI 3 Articles CGI 4 Articles CGI 5 Articles CGI No disposition Date mutation Nature mutation ... Surface Carrez du 5eme lot Nombre de lots Code type local Type local Identifiant local Surface reelle bati Nombre pieces principales Nature culture Nature culture speciale Surface terrain
0 NaN NaN NaN NaN NaN NaN NaN 1 07/01/2020 Vente ... NaN 0 NaN NaN NaN NaN NaN T NaN 1061.0
1 NaN NaN NaN NaN NaN NaN NaN 1 02/01/2020 Vente ... NaN 0 NaN NaN NaN NaN NaN BT NaN 85.0
2 NaN NaN NaN NaN NaN NaN NaN 1 02/01/2020 Vente ... NaN 0 NaN NaN NaN NaN NaN T NaN 1115.0
3 NaN NaN NaN NaN NaN NaN NaN 1 02/01/2020 Vente ... NaN 0 NaN NaN NaN NaN NaN T NaN 1940.0
4 NaN NaN NaN NaN NaN NaN NaN 1 02/01/2020 Vente ... NaN 0 NaN NaN NaN NaN NaN T NaN 1148.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
3484952 NaN NaN NaN NaN NaN NaN NaN 1 16/12/2020 Vente ... NaN 0 2.0 Appartement NaN 22.0 1.0 S NaN 447.0
3484953 NaN NaN NaN NaN NaN NaN NaN 1 16/12/2020 Vente ... NaN 0 4.0 Local industriel. commercial ou assimilé NaN 100.0 0.0 S NaN 447.0
3484954 NaN NaN NaN NaN NaN NaN NaN 1 16/12/2020 Vente ... NaN 0 3.0 Dépendance NaN 0.0 0.0 S NaN 447.0
3484955 NaN NaN NaN NaN NaN NaN NaN 1 16/12/2020 Vente ... NaN 0 2.0 Appartement NaN 87.0 4.0 S NaN 447.0
3484956 NaN NaN NaN NaN NaN NaN NaN 1 08/10/2020 Adjudication ... NaN 2 2.0 Appartement NaN 21.0 1.0 NaN NaN NaN

3484957 rows × 43 columns

Clean data¶

In [120]:
columns_to_keep = ['Date mutation','Nature mutation','Valeur fonciere','Code postal','Commune','Code departement','Code commune','Nombre de lots','Code type local','Type local','Surface reelle bati','Nombre pieces principales','Surface terrain']
fullData['Date mutation'] = pd.to_datetime(fullData['Date mutation'])
fullData['Code departement'] = fullData['Code departement'].astype(str)
fullData = fullData[columns_to_keep]
fullData = fullData.dropna()
fullData['Valeur fonciere'] = pd.to_numeric(fullData['Valeur fonciere'].str.replace(',', '.'))
fullData
Out[120]:
Date mutation Nature mutation Valeur fonciere Code postal Commune Code departement Code commune Nombre de lots Code type local Type local Surface reelle bati Nombre pieces principales Surface terrain
11 2020-09-01 Vente 72000.0 1270.0 COLIGNY 1 108 0 1.0 Maison 35.0 2.0 381.0
13 2020-06-01 Vente 180300.0 1000.0 BOURG-EN-BRESSE 1 53 0 1.0 Maison 75.0 4.0 525.0
16 2020-03-01 Vente 350750.0 1000.0 SAINT-DENIS-LES-BOURG 1 344 0 3.0 Dépendance 0.0 0.0 1267.0
17 2020-03-01 Vente 350750.0 1000.0 SAINT-DENIS-LES-BOURG 1 344 0 1.0 Maison 201.0 7.0 1267.0
18 2020-03-01 Vente 350750.0 1000.0 SAINT-DENIS-LES-BOURG 1 344 0 1.0 Maison 201.0 7.0 1497.0
... ... ... ... ... ... ... ... ... ... ... ... ... ...
3484951 2020-12-16 Vente 1937500.0 75004.0 PARIS 04 75 104 0 2.0 Appartement 32.0 2.0 447.0
3484952 2020-12-16 Vente 1937500.0 75004.0 PARIS 04 75 104 0 2.0 Appartement 22.0 1.0 447.0
3484953 2020-12-16 Vente 1937500.0 75004.0 PARIS 04 75 104 0 4.0 Local industriel. commercial ou assimilé 100.0 0.0 447.0
3484954 2020-12-16 Vente 1937500.0 75004.0 PARIS 04 75 104 0 3.0 Dépendance 0.0 0.0 447.0
3484955 2020-12-16 Vente 1937500.0 75004.0 PARIS 04 75 104 0 2.0 Appartement 87.0 4.0 447.0

1002019 rows × 13 columns

Argent total dépensé par mois selon les types de mutation pendant l'année¶

In [121]:
MUTATIONS = fullData['Nature mutation'].unique()
def plotMutations(mut, data, ax):

    for m in MUTATIONS:
        temp = data[data['Nature mutation'] == m]
        result = temp.groupby(temp['Date mutation'].dt.to_period("M"))['Valeur fonciere'].sum()
        result.index = result.index.to_timestamp()
        x = result.index
        y = result.values
        
        if m == mut:
            ax.plot(x, y, color="#0b53c1", lw=2.4, zorder=10)
            ax.scatter(x, y, fc="w", ec="#0b53c1", s=60, lw=2.4, zorder=12)  
            ax.autoscale()    
        else:
            ax.plot(x, y, color="#BFBFBF", lw=1.5)
    
    ax.set_title(mut, fontfamily="Inconsolata", fontsize=14, fontweight=500)
    return ax
In [122]:
fig, axes = plt.subplots(2, 3, figsize=(14, 7.5))
for idx, (ax, mut) in enumerate(zip(axes.ravel(), MUTATIONS)):
    # Only annotate the first panel
    annotate = idx == 0
    plotMutations(mut, fullData, ax)

On remarque que la plupart des mutations au cours de l'année sont des ventes

In [123]:
data1 = fullData[fullData['Nature mutation'] =='Vente'] 
data1 = data1.groupby(by='Date mutation',sort='Date mutation')['Valeur fonciere'].count()
#data1.index = data1.index.to_timestamp()

data2 = fullData[fullData['Nature mutation'] =='Vente terrain à bâtir'] 
data2 = data2.groupby(by='Date mutation',sort='Date mutation')['Valeur fonciere'].count()
#data2.index = data2.index.to_timestamp()

data3 = fullData[fullData['Nature mutation'] =='Echange'] 
data3 = data3.groupby(by='Date mutation',sort='Date mutation')['Valeur fonciere'].count()
#data3.index = data3.index.to_timestamp()

data4 = fullData[fullData['Nature mutation'] =="Vente en l'état futur d'achèvement"] 
data4 = data4.groupby(by='Date mutation',sort='Date mutation')['Valeur fonciere'].count()
#data4.index = data4.index.to_timestamp()

data5 = fullData[fullData['Nature mutation'] =='Adjudication'] 
data5 = data5.groupby(by='Date mutation',sort='Date mutation')['Valeur fonciere'].count()
#data5.index = data5.index.to_timestamp()

data6 = fullData[fullData['Nature mutation'] =='Expropriation'] 
data6 = data6.groupby(by='Date mutation',sort='Date mutation')['Valeur fonciere'].count()
#data6.index = data6.index.to_timestamp()

plt.figure(figsize=(18,10))
plt.plot(data1.index, data1.values, "r--", color="red")
plt.plot(data2.index, data2.values, "r--", color="blue")
plt.plot(data3.index, data3.values, "r--", color="green")
plt.plot(data4.index, data4.values, "r--", color="yellow")
plt.plot(data5.index, data5.values, "r--", color="purple")
plt.plot(data6.index, data6.values, "r--", color="black")
plt.legend(['Vente','Vente terrain à bâtir', 'Echange',"Vente en l'état futur d'achèvement",'Adjudication','Expropriation'])
plt.title('Nombre de mutations par type au cours des mois, en cumulé')
plt.show()

On peut encore une fois confirmer que le seul type de mutation importante est la vente.

Nombre et répartitions des types de locaux¶

In [124]:
data = fullData.groupby(['Type local'])['Type local'].count()


plt.bar(data.index, data.values)
bars = ['Appartement', 'Dépendance', 'Industriel', 'Maison']
y_pos = np.arange(len(bars))
plt.xticks(y_pos, bars)
plt.title('Nombre de mutations par type de local')
Out[124]:
Text(0.5, 1.0, 'Nombre de mutations par type de local')
In [125]:
perc = [f'{i/data.values.sum()*100:5.2f}%' for i in data.values]
lbl = [f'{j[0]} = {j[1]}' for j in zip(data.index, perc)]

squarify.plot(sizes=data.values, label=lbl)
plt.axis('off')
plt.title('Proportion des types de locaux sur le nombre total de mutations')
plt.show()

On remarque que les mutations concernent principalement des maisons et des appartements.

In [126]:
data=fullData[fullData["Surface terrain"]< 5000]
plt.figure(figsize=(18,10))
plt.xticks(rotation=25)
sbn.violinplot(x = "Type local",y="Surface terrain", data=data)
plt.title('Répartition des types de locaux selon la surface de leur terrain')
Out[126]:
Text(0.5, 1.0, 'Répartition des types de locaux selon la surface de leur terrain')
In [127]:
data=fullData[(fullData["Surface reelle bati"]< 1000) & (fullData["Surface reelle bati"].notna())]
plt.figure(figsize=(18,10))
plt.xticks(rotation=25)
sbn.violinplot(x = "Type local",y="Surface reelle bati", data=data)
plt.title('Répartition des types de locaux selon leur surface réelle bâtie')
Out[127]:
Text(0.5, 1.0, 'Répartition des types de locaux selon leur surface réelle bâtie')

On voit ici que les données des dépendances sont assez peu intéressantes, puisque leur surface réelle bâtie est proche de zéro.

In [128]:
data = fullData[fullData['Valeur fonciere'] < 2000000]
plt.figure(figsize=(18,10))
plt.xticks(rotation=25)
sbn.violinplot(x="Type local",y="Valeur fonciere",data=data)
plt.title('Répartition des types de locaux selon leur valeur foncière')
Out[128]:
Text(0.5, 1.0, 'Répartition des types de locaux selon leur valeur foncière')

Analyse des données par département¶

In [129]:
data = fullData.groupby(['Code departement'])['Nature mutation'].count().sort_values(ascending=True)
plt.figure(figsize=(10,20))

plt.hlines(y=data.index, xmin=0, xmax=data.values, color='purple')
plt.plot(data.values, data.index, "o", color="gold")
 
# Add titles and axis names
#plt.yticks(data.index, data.index)
plt.title('Nombre de mutations par département')
plt.xlabel('Nombre de mutations')
plt.ylabel('Numéros de départements')
#data.plot.barh()
plt.show()
In [130]:
data1 = fullData.groupby(['Code departement'])['Nature mutation'].count()

data = fullData[((fullData['Nature mutation']=='Vente') & ((fullData['Type local'] == 'Maison') | (fullData['Type local'] == 'Appartement')))]
data = fullData.groupby(['Code departement'])['Nature mutation'].count()
plt.figure(figsize=(10,20))

plt.hlines(y=data1.index, xmin = 0, xmax = data1.values, color='red')
plt.hlines(y=data.index, xmin=0, xmax=data.values, color='skyblue')
plt.plot(data.values, data.index, "o")
plt.plot(data1.values, data1.index, "x", color="white")
 
# Add titles and axis names
#plt.yticks(data.index, data.index)
plt.title("Nombre de mutations par département (ronds) et nombre de mutations par département en considérant uniquement les ventes d'appartements et de maisons (croix)")
plt.xlabel('Nombre de mutations')
plt.ylabel('Numéros de départements')
#data.plot.barh()
plt.show()

On ne fait ici que confirmer visuellement que les ventes d'appartement et de maisons constituent la majorité écrasante de mutations, avec une variation extrêmement faible pour certains départements.

In [131]:
myscale = None

def mapping_france_folium(data):
    map = folium.Map(location=[48.862, 2.346], zoom_start = 5)
    departments = f"https://france-geojson.gregoiredavid.fr/repo/departements.geojson"
    d = {'Code': data.index, 'Valeur': np.log(data.values)}
    da = pd.DataFrame(d)

    folium.Choropleth(geo_data=departments, 
    data=da, 
    columns=['Code', 'Valeur'], 
    key_on='properties.code',
    fill_color= "PuRd",
    fill_opacity=1,
    line_opacity=.1).add_to(map)
    
    folium.LayerControl().add_to(map)
    return map


def mapping_Paris_circle(data, bigNumbers = False):
    map = folium.Map(location = [48.856578, 2.351828], zoom_start = 12)
    arr = json.load(open("arrondissements.geojson"))
    d = {'Code': data.index, 'Valeur': data.values}
    da = pd.DataFrame(d)
    for a in arr["features"]:
        prop = a["properties"]
        temp = da[da['Code'] == prop["c_arinsee"] - 100]
        temp = temp['Valeur'].values
        folium.Circle(prop["geom_x_y"], 
        fill=True,
        popup = prop["l_ar"],
        radius = (temp[0]/1) if not bigNumbers else temp[0]/9000000).add_to(map)
    return map


def mapping_Paris(data):
    map = folium.Map(location = [48.856578, 2.351828], zoom_start = 12)
    arr = json.load(open("arrondissements.geojson"))
    d = {'Code': data.index + 100, 'Valeur': np.log(data.values)}
    da = pd.DataFrame(d)
    da = da[(da['Code'] >= 75100) & (da['Code'] <= 75120)]
    myscale = np.linspace(da['Valeur'].min(), da['Valeur'].max(), 10)
    folium.Choropleth(geo_data=arr, 
            data=da, 
            columns=['Code', 'Valeur'], 
            key_on='properties.c_arinsee',
            fill_color= "PuRd",
            threshold_scale=myscale,
            fill_opacity=0.8,
            line_opacity=.1).add_to(map)
        
    
    folium.LayerControl().add_to(map)
    return map

def mapping_Lyon(data):
    map = folium.Map(location = [45.763420, 4.834277], zoom_start = 12)
    arr = json.load(open("adr_voie_lieu.json"))
    d = {'Code': data.index + 380, 'Valeur': np.log(data.values)}
    da = pd.DataFrame(d)
    da = da[(da['Code'] >= 69381) & (da['Code'] <= 69389)]
    folium.Choropleth(geo_data=arr, 
            data=da, 
            columns=['Code', 'Valeur'], 
            key_on='properties.insee',
            fill_color= "PuRd",
            threshold_scale=myscale,
            fill_opacity=0.8,
            line_opacity=.1).add_to(map)
    
    folium.LayerControl().add_to(map)
    return map

def mapping_Marseille(data):
    map = folium.Map(location = [43.296482, 5.36978], zoom_start = 12)
    arr = json.load(open("quartiers-marseille.geojson"))
    d = {'Code': data.index + 200, 'Valeur': np.log(data.values)}
    da = pd.DataFrame(d)
    da = da[(da['Code'] >= 13201) & (da['Code'] <= 13216)]
    da['Code'] = da['Code'].astype(int).astype(str)
    folium.Choropleth(geo_data=arr, 
            data=da, 
            columns=['Code', 'Valeur'], 
            key_on='properties.DEPCO',
            fill_color= "PuRd",
            threshold_scale=myscale,
            fill_opacity=0.8,
            line_opacity=.1).add_to(map)
    
    folium.LayerControl().add_to(map)
    return map
In [132]:
data = fullData.groupby(['Code departement'])['Nature mutation'].count()
map = mapping_france_folium(data)
map
Out[132]:
Make this Notebook Trusted to load map: File -> Trust Notebook

On voit ici, en échelle logarithmique, le nombre de mutations par département au cours de l'année.

In [133]:
data = fullData[fullData['Nature mutation'] == 'Vente'].groupby(['Code departement'])['Valeur fonciere'].sum()
map = mapping_france_folium(data)
map
Out[133]:
Make this Notebook Trusted to load map: File -> Trust Notebook

On voit ci-dessus, en échelle logarithmique, la valeur cumulée des ventes par département au cours de l'année.

In [134]:
data = fullData[fullData['Nature mutation'] == 'Vente']
data = data[data['Type local'] == 'Maison'].groupby(['Code departement'])['Valeur fonciere'].sum()
map = mapping_france_folium(data)
map
Out[134]:
Make this Notebook Trusted to load map: File -> Trust Notebook

On voit ci-dessus, en échelle logarithmique, la valeur cumulée des ventes de maisons par département au cours de l'année. Il est intéressant de noter que l'importance de Paris dans la carte précédente disparaît : très peu de maisons sont vendues à Paris même.

In [135]:
data = fullData[(fullData['Nature mutation'] == 'Vente') & ((fullData['Type local'] == 'Maison') | (fullData['Type local'] == 'Appartement'))]
data['prix_m2'] = data['Valeur fonciere']/data['Surface reelle bati']
data = data.groupby(['Code departement'])['prix_m2'].mean()
#data = data.to_frame()
map = mapping_france_folium(data)
map
Out[135]:
Make this Notebook Trusted to load map: File -> Trust Notebook

On voit ci-dessus, en échelle logarithmique, le prix au m2 par département. On remarque une certaine corrélation entre le nombre de ventes par département et le prix au m2, qu'on cherchera à confirmer par la suite

In [136]:
data = fullData[['Surface reelle bati','Valeur fonciere']]
plt.figure(figsize=(18,10))
plt.scatter(data['Surface reelle bati'],data['Valeur fonciere'])
plt.title('Répartition de la valeur foncière en fonction de la surface bâtie')
plt.show()

On cherche ici à montrer l'existence de valeurs extrêmes dans les données, qui nous ont forcé à adopter une échelle logarithmique pour les cartes, sans quoi nous aurions dû filtrer ces valeurs extrême (ci-dessous).

In [137]:
data = fullData[['Surface reelle bati','Valeur fonciere']]
data = data[data['Valeur fonciere'] < 1000000]
data = data[data['Surface reelle bati'] < 2000]
plt.figure(figsize=(18,10))
plt.scatter(data['Surface reelle bati'],data['Valeur fonciere'])
plt.title('Répartition de la valeur foncière en fonction de la surface bâtie, sans les valeurs extrêmes')
plt.show()

Analyse plus localisée sur Paris¶

Nombre de mutations par arrondissement¶

In [138]:
data = fullData.groupby(['Code postal'])['Nature mutation'].count()
map = mapping_Paris_circle(data, False)
map
Out[138]:
Make this Notebook Trusted to load map: File -> Trust Notebook

On voit ci-dessus le nombre de mutations par arrondissement de Paris. Ci-dessous, on représente les mêmes données sur échelle logarithmique.

In [139]:
data = fullData.groupby(['Code postal'])['Nature mutation'].count()
map = mapping_Paris(data)
map
Out[139]:
Make this Notebook Trusted to load map: File -> Trust Notebook

Valeur des ventes par arrondissement¶

In [140]:
data = fullData[fullData['Nature mutation'] == 'Vente']
data = data.groupby(['Code postal'])['Valeur fonciere'].sum()
map = mapping_Paris_circle(data, True)
map
Out[140]:
Make this Notebook Trusted to load map: File -> Trust Notebook

On voit ci-dessus la valeur des ventes par arrondissement de Paris. Ci-dessous, on représente les mêmes données sur échelle logarithmique.

On va s'intéresser par la suite à la valeur des ventes par arrondissement entre Paris, Lyon et Marseille. On pourra observer que la localisation des quartiers les plus recherchés est bien différente entre ces grandes villes.

In [141]:
data = fullData[fullData['Nature mutation'] == 'Vente']
data = data.groupby(['Code postal'])['Valeur fonciere'].sum()
map = mapping_Paris(data)
map
Out[141]:
Make this Notebook Trusted to load map: File -> Trust Notebook
In [142]:
data = fullData[fullData['Nature mutation'] == 'Vente']
data = data.groupby(['Code postal'])['Valeur fonciere'].sum()
map = mapping_Marseille(data)
map
Out[142]:
Make this Notebook Trusted to load map: File -> Trust Notebook
In [143]:
data = fullData[fullData['Nature mutation'] == 'Vente']
data = data.groupby(['Code postal'])['Valeur fonciere'].sum()
map = mapping_Lyon(data)
map
Out[143]:
Make this Notebook Trusted to load map: File -> Trust Notebook

¶

In [144]:
data = fullData[['Valeur fonciere','Surface reelle bati','Surface terrain', 'Nombre pieces principales']]

sbn.heatmap(data.corr(), annot= True, cmap='Reds')
plt.xticks(rotation = 45)
plt.title('Corrélation entre la valeur foncière, la surface réelle bâtie, \nla surface du terrain et le nombre de pièces principales\n(en France))')
plt.show()
In [145]:
data = fullData[fullData['Code departement'] == '75']
data = data[['Valeur fonciere','Surface reelle bati','Surface terrain', 'Nombre pieces principales']]
sbn.heatmap(data.corr(), annot= True, cmap='Reds')
plt.xticks(rotation = 45)
plt.title('Corrélation entre la valeur foncière, la surface réelle bâtie, \nla surface du terrain et le nombre de pièces principales\n(en IDF))')
plt.show()

On remarquera une grande différence entre les données de corrélation en France, et en Île de France. Particulièrement entre la valeur foncière, la surface réelle bâtie, et la surface du terrain.